package com.spring.theory.mvc.basic.resolver;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockPart;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletCookieValueMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver;

/**
 * 参数解析器原理
 *
 * @author H
 * @version 1.0
 * @date 2024-01-15 16:52
 */
@Slf4j
public class TestResolver {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(/*ResolverWebConfig.class*/);
        DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
        final HttpServletRequest mockRequest = mockRequest();

        /**
         * 1.控制器方法封装成 HandlerMethod
         */
        HandlerMethod handlerMethod = new HandlerMethod(new ResolverController(), ResolverController.class.getMethod("test", String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class, String.class, String.class, HttpServletRequest.class, Uer.class, Uer.class, Uer.class));
        /**
         * 2.对象绑定与类型转换；如字符串转换成int类型等；@RequestParam传入字符串 “18”，但接收的参数类型是 int；如果不添加对象绑定工厂，则无法将字符串转换成int类型
         */
        ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);
        /**
         * 3.准备 ModelAndViewContainer 用来存储中间 Model 结果
         */
        ModelAndViewContainer container = new ModelAndViewContainer();

        /**
         * 4.解析参数值
         */
        for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
            //多个解析器的组合（组合模式）
            HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
            composite.addResolvers(
                    // @RequestParam 解析器
                    new RequestParamMethodArgumentResolver(beanFactory, false),
                    // @PathVariable 解析器
                    new PathVariableMethodArgumentResolver(),
                    // @RequestHeader 解析器
                    new RequestHeaderMethodArgumentResolver(beanFactory),
                    // @CookieValue 解析器
                    new ServletCookieValueMethodArgumentResolver(beanFactory),
                    // @Value 解析器
                    new ExpressionValueMethodArgumentResolver(beanFactory),
                    // HttpServletRequest等特殊值 解析器
                    new ServletRequestMethodArgumentResolver(),
                    // @ModelAttribute 解析器
                    new ServletModelAttributeMethodProcessor(false),
                    // @RequestBody 解析器；需要消息转换器，解析请求体，如json、xml等；
                    // 如果先加入不带注解的 ModelAttribute 解析器，两个解析器都是解析 json 对象，会用错解析器；所以需要先加入 @RequestBody 解析器
                    new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),

                    //省略 ModelAttribute 注解的解析器
                    new ServletModelAttributeMethodProcessor(true),
                    //省略 @RequestParam 注解的解析器
                    new RequestParamMethodArgumentResolver(beanFactory, true)
            );

            //打印参数的注解信息
            String anno = Arrays.stream(parameter.getParameterAnnotations()).map(t -> t.annotationType().getSimpleName()).collect(Collectors.joining());
            anno = anno.isEmpty() ? "" : "@" + anno;

            /**
             *  增加参数名称解析器
             *  spring默认的参数名称解析器支持两种方式获取参数名；反射调用方法参数表和用asm技术找到本地变量
             *  - java -parameters 参数可以把参数名存储在方法参数表里；对接口和类都可以使用；可以反射得到方法名
             *  - java -g 参数可以把参数名放在本地变量表里；只对类可用；
             *  注：javap 反编译可以看出区别
             */
            parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());

            String annoInfo = "[" + parameter.getParameterIndex() + "]" + anno + " " + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName();

            if (composite.supportsParameter(parameter)) {
                //解析参数核心逻辑
                Object o = composite.resolveArgument(parameter, container, new ServletWebRequest(mockRequest), binderFactory);
                log.info(annoInfo + " ==> " + o.getClass() + " ==> " + o);
                if (!container.getModel().isEmpty()) {
                    log.info(String.valueOf(container.getModel()));
                }
            } else {
                log.info("不支持解析" + annoInfo);
            }
        }

    }

    private static HttpServletRequest mockRequest() {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("name1", "aaa");
        request.setParameter("name2", "bbb");
        request.addPart(new MockPart("file", "abc", "hello".getBytes(StandardCharsets.UTF_8)));
        Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
        //放入request作用域，方便后续PathVariable解析器获取
        request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);
        request.setContentType("application/json");
        request.setCookies(new Cookie("token", "123456"));
        //ModelAttribute映射对象属性值
        request.setParameter("name", "张三");
        request.setParameter("age", "18");
        request.setContent(("{\n"
                + "                        \"name\":\"李四\",\n"
                + "                        \"age\":20\n"
                + "                    }"
        ).getBytes(StandardCharsets.UTF_8));

        return new StandardServletMultipartResolver().resolveMultipart(request);
    }

    static class ResolverController {
        public void test(
                // name1=张三
                @RequestParam("name1") String name1,
                // name2=李四
                String name2,
                // age=18
                @RequestParam("age") int age,
                //默认值可以用el表达式获取 ${},#{}；spring 获取数据
                @RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1,
                // 上传文件
                @RequestParam("file") MultipartFile file,
                //  /test/124   /test/{id}
                @PathVariable("id") int id,
                @RequestHeader("Content-Type") String header,
                @CookieValue("token") String token,
                // spring 获取数据  ${} #{}
                @Value("${JAVA_HOME}") String home2,
                // 特殊类型数据request, response, session
                HttpServletRequest request,
                //当一个请求来到时，Spring MVC会查看请求的参数，并尝试使用与控制器方法参数上@ModelAttribute注解相匹配的名字找到相应的模型对象，然后 使用请求参数更新它。再一个将对象存储在 ModelAndViewContainer 中
                @ModelAttribute("abc") Uer user1,
                // name=zhang&age=18
                Uer user2,
                // json
                @RequestBody Uer user3
        ) {
        }
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Uer {
        private String name;
        private String age;
    }

    @Configuration
    public class ResolverWebConfig {

    }

}
